#import "EJConvert.h"

NSString *JSValueToNSString(JSContextRef ctx, JSValueRef v) {
    JSStringRef jsString = JSValueToStringCopy(ctx, v, NULL);
    if (!jsString) return nil;

    NSString *string = (NSString *)JSStringCopyCFString(kCFAllocatorDefault, jsString);
    [string autorelease];
    JSStringRelease(jsString);

    return string;
}

JSValueRef NSStringToJSValue(JSContextRef ctx, NSString *string) {
    JSStringRef jstr = JSStringCreateWithCFString((CFStringRef)string);
    JSValueRef ret = JSValueMakeString(ctx, jstr);
    JSStringRelease(jstr);
    return ret;
}

// JSValueToNumberFast blindly assumes that the given JSValueRef is a
// a number. Everything else will be silently converted to 0.
// This functions comes in a 64bit and 32bit flavor, since the NaN-Boxing
// in JSC works a bit differently on each platforms. For an explanation
// of the taggging refer to JSC/runtime/JSCJSValue.h
// The 32bit version just calls the normal JSValueToNumber() function
// and is thus a lot slower.

double JSValueToNumberFast(JSContextRef ctx, JSValueRef v) {
#if __LP64__ // arm64 version
    union {
        int64_t asInt64;
        double asDouble;
        struct {
            int32_t asInt;
            int32_t tag;
        } asBits;
    } taggedValue = {.asInt64 = (int64_t)v};

    #define DoubleEncodeOffset 0x1000000000000ll
    #define TagTypeNumber      0xffff0000
    #define ValueTrue          0x7

    if ((taggedValue.asBits.tag & TagTypeNumber) == TagTypeNumber) {
        return taggedValue.asBits.asInt;
    } else if (taggedValue.asBits.tag & TagTypeNumber) {
        taggedValue.asInt64 -= DoubleEncodeOffset;
        return taggedValue.asDouble;
    } else if (taggedValue.asBits.asInt == ValueTrue) {
        return 1.0;
    } else {
        return 0; // false, undefined, null, object
    }
#else // armv7 version
    return JSValueToNumber(ctx, v, NULL);
#endif
}

void JSValueUnprotectSafe(JSContextRef ctx, JSValueRef v) {
    if (ctx && v) {
        JSValueUnprotect(ctx, v);
    }
}

JSValueRef NSObjectToJSValue(JSContextRef ctx, NSObject *obj) {
    JSValueRef ret = NULL;

    // String
    if ([obj isKindOfClass:NSString.class]) {
        ret = NSStringToJSValue(ctx, (NSString *)obj);
    }

    // Number or Bool
    else if ([obj isKindOfClass:NSNumber.class]) {
        NSNumber *number = (NSNumber *)obj;
        if (strcmp(number.objCType, @encode(BOOL)) == 0) {
            ret = JSValueMakeBoolean(ctx, number.boolValue);
        } else {
            ret = JSValueMakeNumber(ctx, number.doubleValue);
        }
    }

    // Date
    else if ([obj isKindOfClass:NSDate.class]) {
        NSDate *date = (NSDate *)obj;
        JSValueRef timestamp = JSValueMakeNumber(ctx, date.timeIntervalSince1970 * 1000.0);
        ret = JSObjectMakeDate(ctx, 1, &timestamp, NULL);
    }

    // Array
    else if ([obj isKindOfClass:NSArray.class]) {
        NSArray *array = (NSArray *)obj;
        JSValueRef *args = malloc(array.count * sizeof(JSValueRef));
        for (int i = 0; i < array.count; i++) {
            args[i] = NSObjectToJSValue(ctx, array[i]);
        }
        ret = JSObjectMakeArray(ctx, array.count, args, NULL);
        free(args);
    }

    // Dictionary
    else if ([obj isKindOfClass:NSDictionary.class]) {
        NSDictionary *dict = (NSDictionary *)obj;
        ret = JSObjectMake(ctx, NULL, NULL);
        for (NSString *key in dict) {
            JSStringRef jsKey = JSStringCreateWithUTF8CString(key.UTF8String);
            JSValueRef value = NSObjectToJSValue(ctx, dict[key]);
            JSObjectSetProperty(ctx, (JSObjectRef)ret, jsKey, value, NULL, NULL);
            JSStringRelease(jsKey);
        }
    }

    return ret ? ret : JSValueMakeNull(ctx);
}

NSObject *JSValueToNSObject(JSContextRef ctx, JSValueRef value) {
    JSType type = JSValueGetType(ctx, value);

    switch (type) {
        case kJSTypeString: return JSValueToNSString(ctx, value);
        case kJSTypeBoolean: return [NSNumber numberWithBool:JSValueToBoolean(ctx, value)];
        case kJSTypeNumber: return [NSNumber numberWithDouble:JSValueToNumberFast(ctx, value)];
        case kJSTypeNull: return nil;
        case kJSTypeUndefined: return nil;
        case kJSTypeObject: break;
    }

    if (type == kJSTypeObject) {
        JSObjectRef jsObj = (JSObjectRef)value;

        // Get the Array constructor to check if this Object is an Array
        JSStringRef arrayName = JSStringCreateWithUTF8CString("Array");
        JSObjectRef arrayConstructor = (JSObjectRef)JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), arrayName, NULL);
        JSStringRelease(arrayName);

        if (JSValueIsInstanceOfConstructor(ctx, jsObj, arrayConstructor, NULL)) {
            // Array
            JSStringRef lengthName = JSStringCreateWithUTF8CString("length");
            int count = JSValueToNumberFast(ctx, JSObjectGetProperty(ctx, jsObj, lengthName, NULL));
            JSStringRelease(lengthName);

            NSMutableArray *array = [NSMutableArray arrayWithCapacity:count];
            for (int i = 0; i < count; i++) {
                NSObject *obj = JSValueToNSObject(ctx, JSObjectGetPropertyAtIndex(ctx, jsObj, i, NULL));
                [array addObject:(obj ? obj : NSNull.null)];
            }
            return array;
        } else {
            // Plain Object
            JSPropertyNameArrayRef properties = JSObjectCopyPropertyNames(ctx, jsObj);
            size_t count = JSPropertyNameArrayGetCount(properties);

            NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:count];
            for (size_t i = 0; i < count; i++) {
                JSStringRef jsName = JSPropertyNameArrayGetNameAtIndex(properties, i);
                NSObject *obj = JSValueToNSObject(ctx, JSObjectGetProperty(ctx, jsObj, jsName, NULL));

                NSString *name = (NSString *)JSStringCopyCFString(kCFAllocatorDefault, jsName);
                dict[name] = obj ? obj : NSNull.null;
                [name release];
            }

            JSPropertyNameArrayRelease(properties);
            return dict;
        }
    }

    return nil;
}
